﻿// ______________________________________________________________
//
// SoundManager class - Liam O'Donnell
// Basic sound management container for playing/controlling sound
// ______________________________________________________________
//

// ______________________________________________________________
//
// USAGE:
// - Use SoundManager.create(insideMC) to instantiate the class
// - Play loops or spot effects with addSoundLoop() or playSound()
// - Play external MP3 files with loadAndPlay()
// - Control sounds with fadeSoundTo(), panTo(), panAndFadeTo()
// - Set up ambient random triggers with startRandomSounds()
// - Other methods include: fadeOut(), mono(), mute(), pause()
// - Clear intervals and kill all sounds with kill() or remove()
// - Refer to each method for a brief explanation and usage info
// ______________________________________________________________
//

import com.spikything.audio.*;
import com.spikything.utils.SpikyTools;

class com.spikything.audio.SoundManager extends MovieClip {

	// ______________________________________________________
	//
	// CONSTANTS
	// ______________________________________________________
	//

	public 	var snd			:Sound;					// Global sound object for this SoundManager
	public 	var soundTarget	:MovieClip; 			// Where the sound object is targetting
	public	var myVolume 	:Number; 				// Current volume of our sound sprite (ignoring muting)
	public	var muted 		:Boolean 	= false;	// Specifies if this sound container or sprite is muted
	public 	var paused 		:Boolean 	= false; 	// Specifies if this sound container or sprite paused
	public 	var soundPos 	:Number; 				// Playhead position of this sound container or sprite (only currently updated on pause)
	public 	var soundId 	:String; 				// The linkage ID, or URL of the currently attached sound

	private var maxVol 		:Number 	= 150; 		// The maximum volume sounds should ever play at
	private var targetVol	:Number 	= 100;		// Volume target value
	private var targetPan 	:Number 	= 0; 		// Panner target value
	private var threshold	:Number		= 3;		// Sound fader cut-off threshold
	private var defaultRate	:Number		= .3;		// Default fader speed
	private var fadeRate	:Number;				// How fast the fader is
	private var callback 	:Object; 				// Custom event callback object/function
	private var intervals 	:Array 		= []; 		// List of interval IDs for harvesting on unload
	private var loops 		:Number; 				// Number of loops the current sound sprite will play for
	private var offset 		:Number; 				// Seconds offset the the current sound started at
	private var sndComplete	:Function; 				// Copy of the onSoundComplete handler (if any)

	// ______________________________________________________
	//
	// CONSTRUCTOR
	// ______________________________________________________
	//

	function SoundManager () {};

	// ______________________________________________________
	//
	// EVENT HANDLERS
	// ______________________________________________________
	//

	public function onLoad ():Void { init() };
	public function onUnload ():Void { deInit() };

	// ______________________________________________________
	//
	// STATIC METHODS
	// ______________________________________________________
	//

	public static function create (_base:MovieClip,initObj:Object,depth:Number):MovieClip {
		return SpikyTools.createClass(_base,SoundManager,initObj,null,depth);
	}

	// ______________________________________________________
	//
	// PUBLIC METHODS
	// ______________________________________________________
	//

	// Set where this sound manager's sound object relates to
	public function setTarget (_mc:MovieClip):Void { snd = addSoundObject(_mc) };

	// Fade in or out this sound manager/sprite with optional custom rate and callback
	public function fadeOut (theRate:Number, callback:Object):Void { fadeSoundTo(0,theRate,null,callback) };
	public function fadeIn (theRate:Number, callback:Object):Void { fadeSoundTo(100,theRate,null,callback) };

	// Sets the given sound sprite's muted state (global to this SoundManager if theSound==undefined)
	public function setMute (theSound:MovieClip, myMute:Boolean):Void {
		var sm = getSound(theSound).m;
		sm.muted = myMute;
		volume(sm.myVolume, sm);
	}

	// Control the muting/unmuting of the specified sound sprite
	public function mute (theSound:MovieClip):Void { setMute(theSound,true) };
	public function unmute (theSound:MovieClip):Void { setMute(theSound,false) };
	public function toggleMute (theSound:MovieClip):Void { setMute(theSound,!getSound(theSound).m.muted) };

	// Adds a container with looping sound id 'mySound' and volume 'vol'
	public function addSoundLoop (mySound:String, vol:Number, pan:Number):MovieClip {
		return playSound(mySound,99999,0,vol,pan);
	}

	// Plays a sound of id 'mySound' '_loops' times starting at '_offset' at '_vol' volume with optional '_delay'
	public function playSound (mySound:String, _loops:Number, _offset:Number, _vol:Number, _pan:Number, endCallback:Object, _delay:Number):MovieClip {
		var v = _vol==undefined ? 100 : _vol;
		var _pan = _pan==undefined ? 0 : Math.min(Math.max(-100,_pan),100);
		if (v<=0) return;
		var m = addSoundSprite();
		if (_delay!=undefined) {
			playSoundDelay(mySound, _delay, _pan, _vol, null, endCallback);
			return m;
		}
		var s = m.snd;
		s.attachSound(mySound);
		m.soundId = mySound;
		if (endCallback!=undefined) s.onSoundComplete = function(){m.unloadMovie();endCallback.scope[endCallback.func](endCallback.args,m)};
		volume(v,m);
		s.setPan(_pan);
		s.start(_offset,_loops);
		m.loops = _loops;
		m.offset = _offset;
		m.vol = _vol;
		m.pan = _pan;
		return m;
	}

	// shortcut to pan playsound + switched vol
	public function playSoundPan (mySound:String, pan:Number, vol:Number, endCallback:Object):MovieClip {
		return playSound(mySound, 0, 0, vol, pan, endCallback);
	}

	// delay in seconds for sound to start + pan and vol switched
	public function playSoundDelay (mySound:String, delay:Number, pan:Number, vol:Number, startCallback:Object, endCallback:Object):Number {
		var lc = this;
		var intDelay = setInterval(function(){
											clearInterval(intDelay)
											var m = lc.playSound(mySound, 0, 0, vol, pan, endCallback)
											startCallback.scope[startCallback.func](startCallback.args,m)
											}, delay * 1000);
		intervals.push(intDelay);
		return intDelay;
	}

	// Loads and plays an external MP3 'soundURL' with 'isStreaming', 'autoPlay', and 'callback' function or object onLoad event
	public function loadAndPlay (soundURL:String, isStreaming:Boolean, autoPlay:Boolean, callback, endCallback:Object):MovieClip {
		_soundbuftime = 10;
		var m = addSoundSprite();
		var s = m.snd;
		var callType = typeof(callback);
		if (!isStreaming && callType=="function") s.onLoad = function(success) {if(success && autoPlay) s.start(); callback(success)};
		if (!isStreaming && callType=="object") s.onLoad = function(success) {if(success && autoPlay) s.start(); callback.scope[callback.func](callback.args,success,m)};
		if (endCallback!=undefined) s.onSoundComplete = function() {m.unloadMovie(); endCallback.scope[endCallback.func](endCallback.args,m)};
		s.loadSound(soundURL, isStreaming);
		return m;
	}

	// Sets the volume of 'theSound' to 'vol', global if 'theSound' is undefined
	public function volume (vol:Number, theSound:MovieClip):Void {
		if (theSound == undefined) var theSound = this;
		theSound.myVolume = Math.min(Math.max(0,vol),maxVol);
		theSound.snd.setVolume(vol*!theSound.muted);
	}

	// Fades 'theSound' to 'theVol' with 'theRate' ease-out ratio (global if 'theSound'==undefined)
	public function fadeSoundTo (theVol:Number, theRate:Number, theSound:MovieClip, callback:Object):Void {
		if (theSound==undefined) var theSound = this;
		theSound.targetVol = theVol==undefined ? 0 : Math.min(Math.max(0,theVol),maxVol);
		theSound.fadeRate = theRate==undefined ? defaultRate : Math.min(1,Math.max(theRate,0));
		startMixer(theSound,callback);
	}

	// Pans 'theSound' to 'pan' with 'theRate' ease-out ratio (global if 'theSound'==undefined)
	public function panTo (_pan:Number, theRate:Number, theSound:MovieClip, callback:Object):Void {
		if (theSound==undefined) var theSound = this;
		theSound.targetPan = _pan==undefined ? 0 : Math.min(Math.max(-100,_pan),100);
		theSound.fadeRate = theRate==undefined ? defaultRate : Math.min(1,Math.max(theRate,0));
		startMixer(theSound,callback);
	}

	// What it says on the tin, pans and fades the given sound
	public function panAndFadeTo (theVol:Number, _pan:Number, theRate:Number, theSound:MovieClip):Void {
		fadeSoundTo(theVol,theRate,theSound);
		panTo(_pan,theRate,theSound);
	}

	// Mixes stereo channels into mono (good for using stereo to sync 2 tracks)
	public function mono ():Void { levels(50,50) };

	// Set the speaker-independent levels of the left/right stereo channels
	public function levels (left:Number, right:Number):Void {
		snd.setTransform({ll:left, lr:left, rl:right, rr:right});
	}

	// Start randomly triggered linkage/external sounds with random pan and volume
	public function startRandomSounds (soundList:Array, delayFunc:Function, panFunc:Function, volFunc:Function):Number {
		return triggerRandomSound([soundList,delayFunc,panFunc,volFunc]);
	}

	// Used internally to trigger random spot effects
	public function triggerRandomSound (args:Array, lastSnd:MovieClip, mp3:MovieClip):Number {
		var soundList = args[0];	// array of sounds (either linkage ids or external mp3s)
		var delayFunc = args[1];	// function which returns the random delay (in milliseconds)
		var panFunc = 	args[2]; 	// function which returns the pan for the sound
		var volFunc = 	args[3];	// function which returns the volume for the sound
		//if (typeof(lastSnd)=="movieclip" && !random(3)) {lastSnd.panTo(random(200)-100, .1)};
		if (typeof(mp3)=="movieclip") mp3.panAndFadeTo(volFunc(),panFunc(),1);
		var callback = {scope:this, func:"triggerRandomSound", args:args};
		var mysnd = soundList[random(soundList.length)];
		var sm = addSoundSprite();
		var delay = delayFunc();
		//trace("Play:"+mysnd+" delayed by:"+delay);
		if (mysnd.indexOf(".mp3")>-1) {
			var myInterval = setInterval(function(){clearInterval(myInterval); sm.loadAndPlay(mysnd,false,true,callback)}, delay);
			intervals.push(myInterval);
		} else {
			var myInterval = sm.playSoundDelay(mysnd, delay/1000, panFunc(), volFunc(), callback);
		}
		return myInterval;
	}

	// Stop a sound sprite playing and remove it (or just stop this container)
	public function kill (m:MovieClip):Void {
		if (m==undefined) var m = this;
		m.snd.stop();
		m.clearIntervals();
		for (var i in m) if (m[i]!=this) m[i].kill();
		if (m!=this) m.unloadMovie();
	}

	// Kill a sound sprite/container and unload it completely
	public function remove (m:MovieClip):Void {
		var m = m==undefined ? this : getSound(m).m;
		m.kill();
		m.unloadMovie();
	}

	// Harvest any moribund intervals hanging around
	public function clearIntervals ():Void {
		for (var i in intervals) clearInterval(intervals[i]);
	}

	// Set the pause state of the sound container/sprite
	public function setPause (pauseState:Boolean, theSound:MovieClip):Number {
		var ms = theSound==undefined ? this : getSound(theSound).m;
		ms.paused = pauseState;
		if (ms.paused) {
			ms.soundPos = ms.snd.position;
			ms.sndComplete = ms.snd.onSoundComplete;
			ms.snd.stop();
		} else {
			ms.snd.start(ms.soundPos/1000);
			if (ms.loops) {
				ms.snd.onSoundComplete = function(){ ms.snd.start(0,ms.loops-1) };
			} else {
				ms.snd.onSoundComplete = ms.sndComplete;
			}
		}
		return ms.soundPos;
	}

	public function pause ():Number { return setPause(true) };
	public function unpause ():Number { return setPause(false) };
	public function togglePause ():Number { return setPause(!paused) };

	// ______________________________________________________
	//
	// PRIVATE METHODS
	// ______________________________________________________
	//

	// Initialise the SoundManager instance
	private function init ():Void {
		soundTarget = soundTarget==undefined ? this : soundTarget;
		setTarget(soundTarget);
		volume(targetVol);
	}

	// De-initialise the SoundManager instance
	private function deInit ():Void { kill() };

	// Returns an object with sound sprite's MovieClip and Sound object, regardless of input type
	private function getSound (theSound:Object):Object {
		var sm = (theSound==undefined) ? this : theSound;
		if (typeof(theSound)=="object") sm = theSound.container;
		return {m:sm, s:sm.snd};
	}

	// Creates and returns a SoundSprite
	private function addSoundSprite ():MovieClip {
		return SpikyTools.createClass(this,SoundSprite);
	}

	// Starts a loop in a given sound sprite to pan/fade to target levels
	private function startMixer (theSound:MovieClip, myCallBack:Object):Void {
		theSound.callback = myCallBack;
		theSound.onEnterFrame = function () {
			var tVol = myVolume;
			var tPan = snd.getPan();
			var dVol = targetVol-tVol;
			var dPan = targetPan-tPan;
			var nVol = dVol*fadeRate;
			var nPan = dPan*fadeRate;
			if (Math.abs(dVol)<=threshold && Math.abs(dPan)<=threshold) {
				volume(targetVol*!muted);
				snd.setPan(targetPan);
				if (callback!=undefined) callback.scope[callback.func](callback.args);
				delete this.onEnterFrame;
			} else {
				volume((tVol+nVol)*!muted);
				snd.setPan(tPan+nPan);
			}
		}
		theSound.onEnterFrame();
	}

	// Add a sound object to the given movie and tell it which movie it relates to
	private function addSoundObject (_mc:MovieClip):Sound {
		if (_mc==undefined) var _mc = this;
		var s = new Sound(_mc);
		s.container = _mc;
		return s;
	}

	// Generic attachMovie shortcut
	private function attach (_id:String, _initObj:Object, customDepth:Number):MovieClip {
		var d = customDepth==undefined ? this.getNextHighestDepth() : customDepth;
		return attachMovie(_id,"m"+d,d,_initObj);
	}

}